package mirrormonkey.util.annotations.parsing;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;

import mirrormonkey.util.annotations.control.AnnotationOverride;
import mirrormonkey.util.annotations.control.IRClass;
import mirrormonkey.util.annotations.control.IRClass.NoChange;

/**
 * Collects annotations from class hierarchies as well as membersById and their
 * respective annotations.<br>
 * 
 * Please note that by default, <tt>ClassIR</tt> does only collect data that an
 * <tt>AnnotationParser</tt> passes to it. How to interpret the data is still up
 * to the user.
 * 
 * @author Philipp Christian Loewner
 * 
 */
public class ClassIR extends AnnotationIR {

	/**
	 * Contains member IRs, mapped by the classes of the membersById (
	 * <tt>Constructor</tt>, <tt>Field</tt> or <tt>Method</tt>).
	 */
	public final Map<Class<? extends Member>, Collection<MemberIR>> memberIRs;

	/**
	 * Creates a new <tt>ClassIR</tt> that will use the given
	 * <tt>AnnotationParser</tt> to check for presets.
	 * 
	 * @param parser
	 *            the <tt>AnnotationParser</tt> to get presets from
	 * @param forClass
	 *            the class that this <tt>ClassIR</tt> will be created for
	 */
	public ClassIR(AnnotationParser parser, Class<?> forClass) {
		super(parser, forClass);
		memberIRs = new HashMap<Class<? extends Member>, Collection<MemberIR>>() {
			private static final long serialVersionUID = 1L;
			{
				put(Constructor.class, new LinkedList<MemberIR>());
				put(Method.class, new LinkedHashSet<MemberIR>());
				put(Field.class, new LinkedHashSet<MemberIR>());
			}
		};
	}

	/**
	 * Creates a new <tt>ClassIR</tt> that will copy the data from a previous
	 * <tt>ClassIR</tt>. This constructor is to be called from subclasses to
	 * enable converting one subclass of <tt>ClassIR</tt> to another.
	 * 
	 * @param previous
	 *            the <tt>ClassIR</tt> that stored previous data for the class
	 *            hierarchy
	 * @param forClass
	 *            the class that this <tt>ClassIR</tt> will be created for
	 */
	public ClassIR(ClassIR previous, Class<?> forClass) {
		super(previous, forClass);
		memberIRs = previous.memberIRs;
	}

	/**
	 * Called during the prepass whenever an interface is encountered and before
	 * <tt>parseInterfaceMethod</tt> is invoked for its methods.
	 * 
	 * @param c
	 *            the interface that was encountered
	 */
	public void parseInterfaceClass(Class<?> c) {
		parseAnnotatedElement(c);
	}

	/**
	 * Called during the prepass whenever an interface method declaration is
	 * encountered and after <tt>parseInterfaceClass</tt> was invoked for its
	 * declaring interface.
	 * 
	 * @param m
	 *            the method declaration that was encountered
	 */
	public void parseInterfaceMethod(Method m) {
		parseMethod(m);
	}

	/**
	 * Called during the mainpass whenever a class in the hierachy is
	 * encountered and before <tt>parseField</tt> and <tt>parseMethod</tt> are
	 * invoked for its <tt>Field</tt>s and <tt>Method</tt>s.<br>
	 * 
	 * Parses <tt>c</tt>'s annotations.
	 * 
	 * @param c
	 *            the class from the parsed type hierarchy that was encountered
	 */
	public void parseHierarchyClass(Class<?> c) {
		parseAnnotatedElement(c);
	}

	/**
	 * Called during the mainpass whenever a <tt>Field</tt> is encountered that
	 * is declared in one of the types in the parsed class hierarchy. This is
	 * called after <tt>parseHierarchyClass</tt> was invoked for the declaring
	 * class.<br>
	 * 
	 * @param f
	 *            the <tt>Field</tt> that was encountered
	 */
	public void parseField(Field f) {
		MemberIR ir = manageIR(f);
		if (ir != null) {
			ir.parseField(f);
		}
	}

	/**
	 * Called during the mainpass whenever a <tt>Method</tt> is encountered that
	 * is declared in one of the types in the parsed class hierarchy. This is
	 * called after <tt>parseHierarchyClass</tt> was invoked for the declaring
	 * class.<br>
	 * 
	 * @param m
	 *            the <tt>Method</tt> that was encountered
	 */
	public void parseMethod(Method m) {
		MemberIR ir = manageIR(m);
		if (ir != null) {
			ir.parseMethod(m);
		}
	}

	/**
	 * Called during the mainpass whenever a <tt>Constructor</tt> is encountered
	 * that is declared in one of the types in the parsed class hierarchy. This
	 * is called after <tt>parseHierarchyClass</tt> was invoked for the
	 * declaring class.<br>
	 * 
	 * @param c
	 *            the <tt>Constructor</tt> that was encountered
	 */
	public void parseConstructor(Constructor<?> c) {
		MemberIR ir = manageIR(c);
		if (ir != null) {
			ir.parseConstructor(c);
		}
	}

	/**
	 * Removes every <tt>MemberDataIR</tt> that is not valid for
	 * <tt>lastClass</tt>.
	 * 
	 * @param lastClass
	 *            the class from the hierarchy that is actually parsed
	 */
	public void cleanMemberIRS(Class<?> lastClass) {
		for (Class<?> i : new Class<?>[] { Field.class, Method.class,
				Constructor.class }) {
			for (Iterator<MemberIR> it = memberIRs.get(i).iterator(); it
					.hasNext();) {
				if (!it.next().isValidFor(lastClass)) {
					it.remove();
				}
			}
		}
	}

	/**
	 * Manages and returns a <tt>MemberDataIR</tt> matching a given
	 * <tt>member</tt> or <tt>null</tt> if no such IR is present.<br>
	 * 
	 * Basically, this method will handle finding a matching IR, creating a new
	 * one or deleting an existing one. It will parse the two control
	 * annotations <tt>IRClass</tt> and <tt>AnnotationOverride</tt>.
	 * 
	 * @param member
	 *            the member for which to manage the IR
	 * @return the IR matching <tt>member</tt> or <tt>null</tt> if there is
	 *         currently no IR matching <tt>member</tt>
	 */
	public MemberIR manageIR(Member member) {
		// Search for control annotations on both member and its declared
		// presets
		AnnotatedElement element = (AnnotatedElement) member;
		parser.findIRClass.clear();
		parser.findIRClass.parseAnnotatedElement(element);
		// Search for an already present IR that matches member
		MemberIR currentIR = getCurrentIR(member);

		// If we find an AnnotationOverride, forget everything that we know
		// about the matching IR.
		if (currentIR != null
				&& parser.findIRClass
						.getCollectedAnnotation(AnnotationOverride.class) != null) {
			for (Collection<MemberIR> irs : memberIRs.values()) {
				irs.remove(currentIR);
			}
			currentIR = null;

		}

		// If we find an IRClass, create a new IR or replace the current one
		IRClass irc = parser.findIRClass.getCollectedAnnotation(IRClass.class);
		if (irc != null) {
			// Only create a new IR if it is explicitly requested
			if (!irc.value().equals(NoChange.class)) {
				if (currentIR != null) {
					for (Collection<MemberIR> irs : memberIRs.values()) {
						irs.remove(currentIR);
					}
					currentIR = (MemberIR) parser.reflectiveConstructor(
							currentIR, element, irc.value());
				} else {
					currentIR = (MemberIR) parser.reflectiveConstructor(
							element, irc.value());
				}
				// It may be that membersById of this class have to be parsed
				// again.
				// In that case, notify the parser.
				if (currentIR.requireReparse()) {
					parser.setReparseFlag();
				}
				for (Class<?> listenClass : currentIR.getListenMemberClasses()) {
					memberIRs.get(listenClass).add(currentIR);
				}
			} else if (currentIR == null) {
				throw new RuntimeException(NoChange.class
						+ " is only allowed if there is already an IR present.");
			}

			// Start / stop listening and remove declared annotation classes
			currentIR.parseIRClass(irc);
		}
		return currentIR;
	}

	/**
	 * Searches for an IR that matches a given member.
	 * 
	 * @param member
	 *            the member that the IR should match
	 * @return a <tt>MemberDataIR</tt> (or subtype thereof) that matches
	 *         <tt>member</tt> or <tt>null</tt> if none is available
	 */
	public MemberIR getCurrentIR(Member member) {
		for (MemberIR i : memberIRs.get(member.getClass())) {
			if (i.matches(member)) {
				return i;
			}
		}
		return null;
	}

}
